﻿using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace Xnaml {

	public class GraphicsDeviceManager : IGraphicsDeviceService, Microsoft.Xna.Framework.IGraphicsDeviceManager, IDisposable {
		public static readonly List<DepthFormat> WPFCompatibleDepthStencilFormats = new List<DepthFormat>( new DepthFormat[ ] { DepthFormat.Depth24Stencil8, DepthFormat.Depth24 } );
		public static readonly List<SurfaceFormat> WPFCompatibleBackBufferFormats = new List<SurfaceFormat>( new SurfaceFormat[ ] { SurfaceFormat.Color, SurfaceFormat.Rgba1010102, SurfaceFormat.Rg32, SurfaceFormat.Bgra5551, SurfaceFormat.Bgr565 } );
		public static readonly List<int> WPFCompatibleMultiSampleCounts = new List<int>( new int[ ] { 4, 2, 0 } );
		public static int DefaultBackBufferWidth = 640;
		public static int DefaultBackBufferHeight = 480;
		public static GraphicsProfile DefaultGraphicsProfile = GraphicsProfile.Reach;
		public static GraphicsAdapter graphicsadapter;
		private static GraphicsDevice graphicsdevice;
		private static PresentationParameters parameters;
		private static GraphicsProfile profile;
		private static bool dirty = false;
		private static List<GraphicsDeviceManager> managers = new List<GraphicsDeviceManager>();
		private static readonly TimeSpan deviceLostSleepTime = TimeSpan.FromMilliseconds( 50.0 );
		private bool disposed = false;
		private Game game;

		private static event EventHandler<EventArgs> StaticDeviceLost = new EventHandler<EventArgs>( OnStaticDeviceLost );
		private static event EventHandler<EventArgs> StaticDeviceCreated = new EventHandler<EventArgs>( OnStaticDeviceCreated );
		private static event EventHandler<EventArgs> StaticDeviceDisposing = new EventHandler<EventArgs>( OnStaticDeviceDisposing );
		private static event EventHandler<EventArgs> StaticDeviceReset = new EventHandler<EventArgs>( OnStaticDeviceReset );
		private static event EventHandler<EventArgs> StaticDeviceResetting = new EventHandler<EventArgs>( OnStaticDeviceResetting );
		private static event EventHandler<ResourceCreatedEventArgs> StaticResourceCreated = new EventHandler<ResourceCreatedEventArgs>( OnStaticResourceCreated );
		private static event EventHandler<ResourceDestroyedEventArgs> StaticResourceDestroyed = new EventHandler<ResourceDestroyedEventArgs>( OnStaticResourceDestroyed );
		
		public event EventHandler<EventArgs> DeviceLost;
		public event EventHandler<EventArgs> DeviceCreated;
		public event EventHandler<EventArgs> DeviceDisposing;
		public event EventHandler<EventArgs> DeviceReset;
		public event EventHandler<EventArgs> DeviceResetting;
		public event EventHandler<PreparingDeviceSettingsEventArgs> PreparingDeviceSettings;
		public event EventHandler<ResourceCreatedEventArgs> ResourceCreated;
		public event EventHandler<ResourceDestroyedEventArgs> ResourceDestroyed;

		public GraphicsDevice GraphicsDevice {
			get {
				return graphicsdevice;
			}
		}

		public PresentationParameters PresentationParameters {
			get {
				return parameters;
			}
			set {
				if ( WPFCompatibleBackBufferFormats.Contains( value.BackBufferFormat ) &&
					WPFCompatibleDepthStencilFormats.Contains(value.DepthStencilFormat) ) {
					parameters = value;
					dirty = true;
				}
			}
		}

		public GraphicsProfile GraphicsProfile {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.GraphicsProfile;
				return profile;
			}
			set {
				profile = value;
				if ( graphicsdevice != null && graphicsdevice.GraphicsProfile != value )
					dirty = true;
			}
		}

		public DisplayOrientation SupportedOrientations {
			get {
				if (graphicsdevice != null)
					return graphicsdevice.PresentationParameters.DisplayOrientation;
				return parameters.DisplayOrientation;
			}
			set {
				parameters.DisplayOrientation = value;
				if ( graphicsdevice != null && graphicsdevice.PresentationParameters.DisplayOrientation != value )
					dirty = true;
			}
		}

		public bool IsFullScreen {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.PresentationParameters.IsFullScreen;
				return parameters.IsFullScreen;
			}
			set {
				parameters.IsFullScreen = value;
				if ( graphicsdevice != null && graphicsdevice.PresentationParameters.IsFullScreen != value ) {
					dirty = true;
				}
			}
		}

		public int PreferredBackBufferWidth {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.PresentationParameters.BackBufferWidth;
				return parameters.BackBufferWidth;
			}
			set {
				parameters.BackBufferWidth = value;
				if ( graphicsdevice != null && graphicsdevice.PresentationParameters.BackBufferWidth != value ) {
					dirty = true;
				}
			}
		}

		public int PreferredBackBufferHeight {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.PresentationParameters.BackBufferHeight;
				return parameters.BackBufferHeight;
			}
			set {
				parameters.BackBufferHeight = value;
				if ( graphicsdevice != null && graphicsdevice.PresentationParameters.BackBufferWidth != value ) {
					dirty = true;
				}
			}
		}

		public SurfaceFormat PrefferedBackBufferFormat {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.PresentationParameters.BackBufferFormat;
				return parameters.BackBufferFormat;
			}
			set {
				if ( WPFCompatibleBackBufferFormats.Contains( value ) ) {
					parameters.BackBufferFormat = value;
					if ( graphicsdevice != null && graphicsdevice.PresentationParameters.BackBufferFormat != value ) {
						dirty = true;
					}
				}
			}
		}

		public DepthFormat PrefferedDepthStencilFormat {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.PresentationParameters.DepthStencilFormat;
				return parameters.DepthStencilFormat;
			}
			set {
				parameters.DepthStencilFormat = value;
				if ( graphicsdevice != null && graphicsdevice.PresentationParameters.DepthStencilFormat != value ) {
					dirty = true;
				}
			}
		}

		public bool PreferMultiSampling {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.PresentationParameters.MultiSampleCount > 0;
				return parameters.MultiSampleCount > 0;
			}
			set {
				if ( value )
					parameters.MultiSampleCount = 0;
				else
					parameters.MultiSampleCount = 4;
				if ( graphicsdevice != null ) {
					dirty = true;
				}
			}
		}

		public bool SynchronizeWithVerticalRetrace {
			get {
				if ( graphicsdevice != null )
					return graphicsdevice.PresentationParameters.PresentationInterval == PresentInterval.One;
				return parameters.PresentationInterval == PresentInterval.One;
			}
			set {
				if ( value )
					parameters.PresentationInterval = PresentInterval.One;
				else
					parameters.PresentationInterval = PresentInterval.Immediate;
				if ( graphicsdevice != null && graphicsdevice.PresentationParameters.PresentationInterval != parameters.PresentationInterval )
					dirty = true;
			}
		}

		public GraphicsDeviceManager ( Game game ) {
			if ( game == null )
				throw new ArgumentNullException( "The Game class for the GraphicsDeviceManager cannot be null." );
			profile = DefaultGraphicsProfile;
			parameters = new PresentationParameters();
			parameters.BackBufferWidth = Math.Max( (int)game.ActualWidth, 1 );
			parameters.BackBufferHeight = Math.Max( (int)game.ActualHeight, 1 );
			parameters.PresentationInterval = PresentInterval.One;
			parameters.BackBufferFormat = SurfaceFormat.Color;
			parameters.DepthStencilFormat = DepthFormat.Depth24;
			parameters.DeviceWindowHandle = game.Window.Handle;
			parameters.IsFullScreen = false;
			this.game = game;
			game.Window.WindowResize += new EventHandler<EventArgs>( WindowResize );
			game.Services.AddService( typeof( Microsoft.Xna.Framework.IGraphicsDeviceManager ), this );
			game.Services.AddService( typeof( IGraphicsDeviceService ), this );
			managers.Add( this );
		}

		public void ApplyChanges ( ) {
			if ( dirty ) {
				ResetDevice( this );
			}
		}

		private void WindowResize ( object sender, EventArgs e ) {
			ResetDevice();
		}

		private static void OnStaticDeviceCreated ( object sender, EventArgs e ) {
			for ( int i = managers.Count - 1; i > -1; i-- ) {
				managers[ i ].OnDeviceLost( managers[ i ], e );
			}
		}

		private static void OnStaticDeviceLost ( object sender, EventArgs e ) {
			for ( int i = managers.Count - 1; i > -1; i-- ) {
				managers[ i ].OnDeviceLost( managers[ i ], e );
			}
		}

		private static void OnStaticDeviceReset ( object sender, EventArgs e ) {
			for ( int i = managers.Count - 1; i > -1; i-- ) {
				managers[ i ].OnDeviceReset( managers[ i ], e );
			}
		}

		private static void OnStaticDeviceResetting ( object sender, EventArgs e ) {
			for ( int i = managers.Count - 1; i > -1; i-- ) {
				managers[ i ].OnDeviceResetting( managers[ i ], e );
			}
		}

		private static void OnStaticDeviceDisposing ( object sender, EventArgs e ) {
			for ( int i = managers.Count - 1; i > -1; i-- ) {
				managers[ i ].OnDeviceDisposing( managers[ i ], e );
			}
		}

		private static void OnStaticResourceCreated ( object sender, ResourceCreatedEventArgs e ) {
			for ( int i = managers.Count - 1; i > -1; i-- ) {
				managers[ i ].OnResourceCreated( managers[ i ], e );
			}
		}

		private static void OnStaticResourceDestroyed ( object sender, ResourceDestroyedEventArgs e ) {
			for ( int i = managers.Count - 1; i > -1; i-- ) {
				managers[ i ].OnResourceDestroyed( managers[ i ], e );
			}
		}

		protected void OnDeviceCreated ( object sender, EventArgs e ) {
			if ( DeviceCreated != null )
				DeviceCreated( this, e );
		}

		protected void OnDeviceLost ( object sender, EventArgs e ) {
			if ( DeviceLost != null )
				DeviceLost( this, e );
		}

		protected void OnDeviceReset ( object sender, EventArgs e ) {
			if ( DeviceReset != null )
				DeviceReset( this, e );
		}

		protected void OnDeviceResetting ( object sender, EventArgs e ) {
			if ( DeviceResetting != null )
				DeviceResetting( this, e );
		}

		protected void OnDeviceDisposing ( object sender, EventArgs e ) {
			if ( DeviceDisposing != null )
				DeviceDisposing( this, e );
		}

		protected void OnResourceCreated ( object sender, ResourceCreatedEventArgs e ) {
			if ( ResourceCreated != null )
				ResourceCreated( this, e );
		}

		protected void OnResourceDestroyed ( object sender, ResourceDestroyedEventArgs e ) {
			if ( ResourceDestroyed != null )
				ResourceDestroyed( this, e );
		}

		protected void OnPreparingDeviceSettings ( object sender, PreparingDeviceSettingsEventArgs e ) {
			if ( PreparingDeviceSettings != null )
				PreparingDeviceSettings( this, e );
		}

		public void CreateDevice ( ) {
			if ( game == null )
				throw new Exception( "No Game is Present to Create Graphics Device." );
			if ( game.Window == null )
				throw new Exception( "The Game must have a window to present the drawing to." );
			if ( game.Window.Handle == null || game.Window.Handle == IntPtr.Zero )
				throw new Exception( "The Game Window'f Handle must be non-null" );
			CreateDevice( this );
		}

		protected virtual void RankDevices ( List<GraphicsDeviceInformation> foundDevices ) {

		}

		protected bool FindMinimaxSupportedModes ( SurfaceFormat displayformat, DisplayModeCollection potentialmodes, out int SmallestWidth, out int SmallestHeight, out int LargestWidth, out int LargestHeight ) {
			bool anysupportedatall = false;
			LargestHeight = LargestWidth = 0;
			SmallestWidth = SmallestHeight = int.MaxValue;
			foreach ( DisplayMode mode in potentialmodes ) {
				anysupportedatall = true;
				LargestHeight = Math.Max( mode.Height, LargestHeight );
				LargestWidth = Math.Max( mode.Width, LargestWidth );
				SmallestHeight = Math.Min( mode.Height, SmallestHeight );
				SmallestWidth = Math.Min( mode.Width, SmallestWidth );
			}
			return anysupportedatall;
		}

		protected virtual GraphicsDeviceInformation FindBestDevice ( bool anyadapter ) {
			if ( anyadapter ) {
				List<GraphicsDeviceInformation> gdinfos;
				FindBestDevice( this, out gdinfos );
				return gdinfos[ 0 ];
			}
			else if (graphicsadapter != null) {
				GraphicsDeviceInformation info = new GraphicsDeviceInformation( ) { Adapter = graphicsadapter, GraphicsProfile = profile };
				SurfaceFormat sfbest;
				DepthFormat dfbest;
				int mscbest = 0;
				PresentationParameters presentationpotential = null;
				for ( int f = 0; f < WPFCompatibleBackBufferFormats.Count; ++f ) {
					for ( int d = 0; d < WPFCompatibleDepthStencilFormats.Count; ++d ) {
						for ( int s = 0; s < WPFCompatibleMultiSampleCounts.Count; ++s ) {
							if ( info.Adapter.QueryBackBufferFormat( profile, WPFCompatibleBackBufferFormats[ f ], WPFCompatibleDepthStencilFormats[ d ], WPFCompatibleMultiSampleCounts[s],
								out sfbest, out dfbest, out mscbest ) ) {

								int LargestX, LargestY, SmallestX, SmallestY;

								FindMinimaxSupportedModes( WPFCompatibleBackBufferFormats[ f ], info.Adapter.SupportedDisplayModes, out SmallestX, out SmallestY, out LargestX, out LargestY );

								PreferredBackBufferHeight = Math.Max( Math.Min( PreferredBackBufferHeight, LargestY ), SmallestY );
								PreferredBackBufferHeight = Math.Max( Math.Min( PreferredBackBufferWidth, LargestX ), SmallestX );

								presentationpotential = parameters.Clone( );
								presentationpotential.BackBufferFormat = WPFCompatibleBackBufferFormats[ f ];
								presentationpotential.DepthStencilFormat = WPFCompatibleDepthStencilFormats[ d ];
								presentationpotential.MultiSampleCount = WPFCompatibleMultiSampleCounts[ s ];

								info.PresentationParameters = presentationpotential;
								break;
							}
						}
					}

					if ( presentationpotential != null )
						break;
				}
				if (presentationpotential != null)
					return info;
			}
			throw new NoSuitableGraphicsDeviceException( "There is no current graphics device adapter to get the best Device Information from." );
		}

		private void FindBestDevice ( GraphicsDeviceManager graphicsdevicemanager, out List<GraphicsDeviceInformation> gdinfos ) {
			GraphicsAdapter[ ] adapters = new GraphicsAdapter[ GraphicsAdapter.Adapters.Count ];
			gdinfos = new List<GraphicsDeviceInformation>( adapters.Length );
			GraphicsAdapter.Adapters.CopyTo( adapters, 0 );

			for ( int i = 0; i < adapters.Length; ++i ) {
				if ( adapters[ i ] == null || !adapters[ i ].IsProfileSupported( profile ) )
					continue;

				PresentationParameters presentationpotential = null;
				SurfaceFormat sfbest;
				DepthFormat dfbest;
				int mscbest;

				for ( int f = 0; f < WPFCompatibleBackBufferFormats.Count; ++f ) {
					for ( int d = 0; d < WPFCompatibleDepthStencilFormats.Count; ++d ) {
						for ( int s = 0; s < WPFCompatibleMultiSampleCounts.Count; ++s ) {
							if ( adapters[ i ].QueryBackBufferFormat( profile, WPFCompatibleBackBufferFormats[ f ], WPFCompatibleDepthStencilFormats[ d ], WPFCompatibleMultiSampleCounts[ s ],
								out sfbest, out dfbest, out mscbest ) ) {

								int LargestX, LargestY, SmallestX, SmallestY;

								FindMinimaxSupportedModes( WPFCompatibleBackBufferFormats[ f ], adapters[ i ].SupportedDisplayModes, out SmallestX, out SmallestY, out LargestX, out LargestY );

								presentationpotential = parameters.Clone( );
								
								presentationpotential.BackBufferHeight = Math.Max( Math.Min( PreferredBackBufferHeight, LargestY ), SmallestY );
								presentationpotential.BackBufferWidth = Math.Max( Math.Min( PreferredBackBufferWidth, LargestX ), SmallestX );
								presentationpotential.BackBufferFormat = WPFCompatibleBackBufferFormats[ f ];
								presentationpotential.DepthStencilFormat = WPFCompatibleDepthStencilFormats[ d ];
								presentationpotential.MultiSampleCount = WPFCompatibleMultiSampleCounts[ s ];
								break;
							}
						}
					}

					if (presentationpotential != null)
						break;
				}
				if ( presentationpotential == null )
					continue;

				if ( adapters[ i ].IsDefaultAdapter ) {
					gdinfos.Insert( 0, new GraphicsDeviceInformation( ) {
						Adapter = adapters[ i ],
						GraphicsProfile = profile,
						PresentationParameters = presentationpotential
					} );
				}
				else {
					gdinfos.Add( new GraphicsDeviceInformation( ) {
						Adapter = adapters[ i ],
						GraphicsProfile = profile,
						PresentationParameters = presentationpotential
					} );
				}
			}

			if ( gdinfos.Count < 1 ) {
				throw new NoSuitableGraphicsDeviceException(
					String.Format( "There was no graphics device that could support any WPF-Compatible Surface Formats, WPF Compatible Depth Formats, or your specified Graphics Profile ( {0} ).",
					profile ) );
			}

			graphicsdevicemanager.RankDevices( gdinfos );
		}

		private static void CreateDevice ( GraphicsDeviceManager graphicsdevicemanager ) {
			parameters.DeviceWindowHandle = graphicsdevicemanager.game.Window.Handle;

			if ( graphicsdevice != null && !graphicsdevice.IsDisposed && dirty ) {
				ResetDevice( graphicsdevicemanager );
				return;
			}

			GraphicsDeviceInformation best = graphicsdevicemanager.FindBestDevice( true );

			graphicsadapter = best.Adapter;
			profile = best.GraphicsProfile;
			parameters = best.PresentationParameters;

			graphicsdevice = new GraphicsDevice(
				graphicsadapter,
				profile,
				parameters );
			
			OnStaticDeviceCreated( graphicsdevicemanager, EventArgs.Empty );
		}

		public void ResetDevice ( ) {
			ResetDevice( this );
		}

		protected virtual bool CanResetDevice ( GraphicsDeviceInformation newdeviceinfo ) {
			SurfaceFormat sfbest;
			DepthFormat dfbest;
			int mscbest;
			return newdeviceinfo.Adapter != null &&
				newdeviceinfo.Adapter.IsProfileSupported( profile ) &&
				newdeviceinfo.Adapter.QueryBackBufferFormat( profile, newdeviceinfo.PresentationParameters.BackBufferFormat, newdeviceinfo.PresentationParameters.DepthStencilFormat, newdeviceinfo.PresentationParameters.MultiSampleCount,
				out sfbest, out dfbest, out mscbest ) &&
				WPFCompatibleBackBufferFormats.Contains( newdeviceinfo.PresentationParameters.BackBufferFormat ) &&
				WPFCompatibleDepthStencilFormats.Contains( newdeviceinfo.PresentationParameters.DepthStencilFormat );
		}

		private static void ResetDevice ( GraphicsDeviceManager graphicsdevicemanager ) {
			OnStaticDeviceResetting( graphicsdevicemanager, EventArgs.Empty );

			int BestBackBufferWidth = 0;
			int BestBackBufferHeight = 0;

			for ( int i = 0; i < managers.Count; i++ ) {
				BestBackBufferWidth = Math.Max( BestBackBufferWidth, (int)managers[i].game.ActualWidth );
				BestBackBufferHeight = Math.Max( BestBackBufferHeight, (int)managers[i].game.ActualHeight );
			}
			
			parameters.BackBufferWidth = BestBackBufferWidth;
			parameters.BackBufferHeight = BestBackBufferHeight;
			
			if ( graphicsdevicemanager.PreparingDeviceSettings != null ) {
				GraphicsDeviceInformation info = new GraphicsDeviceInformation( ) { Adapter = graphicsadapter, GraphicsProfile = profile, PresentationParameters = parameters };
				graphicsdevicemanager.PreparingDeviceSettings( graphicsdevicemanager, new PreparingDeviceSettingsEventArgs( info ) );
			}

			graphicsdevice.Reset( parameters, graphicsadapter );

			graphicsdevicemanager.game.RenderToRetrace( );

			dirty = false;

			OnStaticDeviceReset( graphicsdevicemanager, EventArgs.Empty );
		}

		public bool BeginDraw ( ) {
			if ( IsFullScreen || !game.IsActive ) {
				return false;
			}
			switch ( graphicsdevice.GraphicsDeviceStatus ) {
				case GraphicsDeviceStatus.Lost:
					Thread.Sleep( (int)deviceLostSleepTime.TotalMilliseconds );
					return false;
				case GraphicsDeviceStatus.NotReset:
					Thread.Sleep( (int)deviceLostSleepTime.TotalMilliseconds );
					ResetDevice( );
					return false;
			}
			return true;
		}

		public void EndDraw ( ) {
			if ( graphicsdevice != null ) {
				Microsoft.Xna.Framework.Rectangle rect =
					new Microsoft.Xna.Framework.Rectangle( 0, 0, (int)Math.Ceiling( game.ActualWidth ), (int)Math.Ceiling( game.ActualHeight ) );
				int topmostright = (int)( game.Window.TopLevelWindow.Left + game.Window.TopLevelWindow.Width - 8 );
				int topmostbottom = (int)( game.Window.TopLevelWindow.Top + game.Window.TopLevelWindow.Height - 8 );
				if ( game.Window.Left + rect.Width > topmostright ) {
					rect.Width -= ( (int)( game.Window.Left + rect.Width ) - topmostright );
					if ( rect.Width < 1 ) {
						return;
					}
				}
				if ( game.Window.Top + rect.Height > topmostbottom ) {
					rect.Height -= ( (int)( game.Window.Top + rect.Height ) - topmostbottom );
					if ( rect.Height < 1 ) {
						return;
					}
				}
				if ( game.ShrinkToFit ) {
					GraphicsDevice.Present( null, null, game.Window.Handle );
				}
				else {
					GraphicsDevice.Present( rect, rect, game.Window.Handle );
				}
			}
		}

		protected virtual void Dispose ( bool disposing ) {
			if ( disposing && !disposed )
				Dispose( );
		}

		public void Dispose ( ) {
			managers.Remove( this );
			if ( managers.Count < 1 )
				graphicsdevice.Dispose( );
			disposed = true;
		}
	}
}